/*
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

package ru.irenproject.pad;

import java.util.List;

import javax.inject.Inject;

import ru.irenproject.Utils;
import ru.irenproject.pad.PadProtos.Block;
import ru.irenproject.pad.PadProtos.BlockType;
import ru.irenproject.pad.PadProtos.ImageBlock;
import ru.irenproject.pad.PadProtos.ImageFormat;
import ru.irenproject.pad.PadProtos.TextBlock;
import ru.irenproject.store.AbstractThing;
import ru.irenproject.store.Event;
import ru.irenproject.store.Stash;

import com.google.protobuf.ByteString;

public final class Pad extends AbstractThing {
  public enum ChangeEvent implements Event { INSTANCE }

  public static final Block LINE_FEED_BLOCK = Block.newBuilder()
      .setType(BlockType.LINE_FEED)
      .build();

  public static Block createTextBlock(String text) {
    return Block.newBuilder()
        .setType(BlockType.TEXT)
        .setTextBlock(TextBlock.newBuilder().setText(text).build())
        .build();
  }

  public static Block createImageBlock(Image image) {
    return Block.newBuilder()
        .setType(BlockType.IMAGE)
        .setImageBlock(ImageBlock.newBuilder().setImage(image.id()).build())
        .build();
  }

  @Stash private PadProtos.Pad.Builder $;

  @Inject Pad() {}

  public int blockCount() {
    return $.getBlockCount();
  }

  public void clear() {
    $.clearBlock();
    post(ChangeEvent.INSTANCE);
  }

  public boolean isEmpty() {
    return blockCount() == 0;
  }

  private Address positionToAddress(int position) {
    Address res = new Address();
    if (position > 0) {
      int blockCount = blockCount();
      res.blockIndex = blockCount;

      int blockStartPosition = 0;
      for (int i = 0; i < blockCount; ++i) {
        // assert: position >= blockStartPosition

        int nextBlockStartPosition = blockStartPosition + getBlockLength($.getBlock(i));
        if (position < nextBlockStartPosition) {
          res.blockIndex = i;
          res.offsetInBlock = position - blockStartPosition;
          break;
        }

        blockStartPosition = nextBlockStartPosition;
      }
    }

    return res;
  }

  private static int getBlockLength(Block block) {
    switch (block.getType()) {
      case TEXT: return block.getTextBlock().getText().length();
      case IMAGE: return 1;
      case LINE_FEED: return 1;
    }
    throw new RuntimeException();
  }

  private void insertBlock(Block block, int position) {
    Address address = positionToAddress(position);
    int insertBeforeBlockIndex;
    if (address.offsetInBlock > 0) {
      String oldText = $.getBlock(address.blockIndex).getTextBlock().getText();
      Block headBlock = createTextBlock(oldText.substring(0, address.offsetInBlock));
      Block tailBlock = createTextBlock(oldText.substring(address.offsetInBlock));

      $.setBlock(address.blockIndex, headBlock);
      insertBeforeBlockIndex = address.blockIndex + 1;
      $.addBlock(insertBeforeBlockIndex, tailBlock);
    } else {
      insertBeforeBlockIndex = address.blockIndex;
    }

    $.addBlock(insertBeforeBlockIndex, block);

    joinTextBlocks(insertBeforeBlockIndex);
    joinTextBlocks(insertBeforeBlockIndex - 1);

    post(ChangeEvent.INSTANCE);
  }

  private void joinTextBlocks(int leftIndex) {
    Block leftBlock, rightBlock;
    if ((leftIndex >= 0) && (leftIndex < blockCount() - 1)
        && ((leftBlock = $.getBlock(leftIndex)).getType() == BlockType.TEXT)
        && ((rightBlock = $.getBlock(leftIndex + 1)).getType() == BlockType.TEXT)) {
      $.setBlock(leftIndex, createTextBlock(leftBlock.getTextBlock().getText()
          + rightBlock.getTextBlock().getText()));
      $.removeBlock(leftIndex + 1);
    }
  }

  public void insertText(String text, int position) {
    insertBlock(createTextBlock(text), position);
  }

  public void insertLineFeed(int position) {
    insertBlock(LINE_FEED_BLOCK, position);
  }

  public void insertImage(ImageFormat imageFormat, byte[] imageData,
      ImageSource source, int position) {
    Image image = Image.obtain(store(), imageFormat,
        ByteString.copyFrom(imageData), source);
    insertBlock(createImageBlock(image), position);
  }

  public void appendText(String text) {
    if (!text.isEmpty()) {
      if (!isEmpty() && ($.getBlock(blockCount() - 1).getType() == BlockType.TEXT)) {
        String oldText = $.getBlock(blockCount() - 1).getTextBlock().getText();
        $.setBlock(blockCount() - 1, createTextBlock(oldText + text));
      } else {
        $.addBlock(createTextBlock(text));
      }
      post(ChangeEvent.INSTANCE);
    }
  }

  public void appendLineFeed() {
    $.addBlock(LINE_FEED_BLOCK);
    post(ChangeEvent.INSTANCE);
  }

  public void appendImage(Image image) {
    $.addBlock(createImageBlock(image));
    post(ChangeEvent.INSTANCE);
  }

  public List<Block> blockList() {
    return $.getBlockList();
  }

  public void deleteFragment(int position, int length) {
    if (length > 0) {
      Address begin = positionToAddress(position);
      Address end = positionToAddress(position + length - 1);

      if ((begin.compareTo(end) <= 0) && (end.blockIndex < blockCount())) {
        if (begin.blockIndex == end.blockIndex) {
          Block.Builder b = $.getBlockBuilder(begin.blockIndex);
          if (b.getType() == BlockType.TEXT) {
            String text = b.getTextBlock().getText();
            String newText = text.substring(0, begin.offsetInBlock) + text.substring(end.offsetInBlock + 1);
            if (newText.isEmpty()) {
              removeBlockAndJoinText(begin.blockIndex);
            } else {
              b.getTextBlockBuilder().setText(newText);
            }
          } else {
            removeBlockAndJoinText(begin.blockIndex);
          }
        } else {
          Block.Builder e = $.getBlockBuilder(end.blockIndex);
          if (e.getType() == BlockType.TEXT) {
            String newText = e.getTextBlock().getText().substring(end.offsetInBlock + 1);
            if (newText.isEmpty()) {
              $.removeBlock(end.blockIndex);
            } else {
              e.getTextBlockBuilder().setText(newText);
            }
          } else {
            $.removeBlock(end.blockIndex);
          }

          for (int i = end.blockIndex - 1; i > begin.blockIndex; --i) {
            $.removeBlock(i);
          }

          Block.Builder b = $.getBlockBuilder(begin.blockIndex);
          if (b.getType() == BlockType.TEXT) {
            String newText = b.getTextBlock().getText().substring(0, begin.offsetInBlock);
            if (newText.isEmpty()) {
              $.removeBlock(begin.blockIndex);
            } else {
              b.getTextBlockBuilder().setText(newText);
            }
          } else {
            $.removeBlock(begin.blockIndex);
          }

          joinTextBlocks(begin.blockIndex);
          joinTextBlocks(begin.blockIndex - 1);
        }

        post(ChangeEvent.INSTANCE);
      }
    }
  }

  private void removeBlockAndJoinText(int blockIndex) {
    $.removeBlock(blockIndex);
    joinTextBlocks(blockIndex - 1);
  }

  public String getBrief(int softLimit, int hardLimit) {
    StringBuilder sb = new StringBuilder();
    Block lastBlock = null;
    int n = $.getBlockCount();
    for (int i = 0; i < n; ++i) {
      Block b = $.getBlock(i);
      if (b.getType() == BlockType.TEXT) {
        sb.append(b.getTextBlock().getText());
      } else if ((lastBlock == null) || (lastBlock.getType() != b.getType())) {
        if (b.getType() == BlockType.LINE_FEED) {
          sb.append(" ");
        } else {
          sb.append("...");
        }
      }
      if (sb.length() > hardLimit) {
        break;
      }
      lastBlock = b;
    }
    return Utils.shortenString(sb.toString(), softLimit, hardLimit);
  }
}
